package de.bloxel.world;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.jme3.asset.AssetManager;
import com.jme3.bounding.BoundingBox;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;

import de.bloxel.core.BloxelFactory.BloxelType;

/**
 * This implementation knows more than one {@link TerrainChunk}, it knows a three-dimensional-chunk-network to simulate
 * a open world environment.
 * 
 * Bildet eine Welt ab, das Zentrum der Welt ist zu beginn der {@link TerrainChunk} 0, wenn sich der player nach und
 * nach bewegt, dann muessen immer alle chunks in seiner umgebung geladen sein.
 * 
 * Wenn wir uns vom urspruenglichen zentrum entfernen, dann werden die koodinaten der geladenen chunks relativ zu
 * urspruenglichen zentrum (0,0,0) angegeben sein, d.h. chunk 0 wird absolut gesehen immer chunk 0 bleiben.
 * 
 * @author andreas
 * @since 0.1.0
 */
public class OpenWorldTerrainChunk implements TerrainChunk {

  private static final Logger LOG = LoggerFactory.getLogger(OpenWorldTerrainChunk.class);

  /**
   * Standard chunk size: 8
   */
  private static final int DEFAULT_CHUNK_SIZE = 8;

  /**
   * Translate world location into chunk location.
   * 
   * XXX ich habe einfach keine andere moeglichkeit gefunden eine eingabe-wert (x, y oder z) auf den entsprechenden
   * chunk-ziel-bereich zu mappen?! Vielleicht kann man das auch direkt ausrechnen?
   * 
   * <pre>
   * hier mal nur fuer den x wert ein beispiel was es zu finden gilt
   * 
   * chunk-size = 4 (in jede richtung)
   * 
   *    chunk   chunk   chunk   chunk   chunk   
   *  -----------------------------------------
   *  V       V       V       V       V       V
   * -|---|---|---|---|---|---|---|---|---|---|-
   * -24 -20 -16 -8  -4   0   4   8   16  20  24
   * 
   * In welchen chunk gehoert nun -1, -2, -8.5, -17 oder 5, 17, 19, 21 usw.
   * 
   * </pre>
   * 
   * @param aLocation
   * @param theChunkSize
   * @return
   */
  public static Vector3f calcChunkLocation(final Vector3f aLocation, final float theChunkSize) {
    final float cs2 = theChunkSize * 2;
    float x1 = -theChunkSize, x2 = theChunkSize;
    float y1 = -theChunkSize, y2 = theChunkSize;
    float z1 = -theChunkSize, z2 = theChunkSize;
    float x = 0;
    float y = 0;
    float z = 0;
    while (true) {
      if (x1 <= aLocation.x && aLocation.x <= x2) {
        break;
      }
      if (aLocation.x > 0) {
        x1 += cs2;
        x2 += cs2;
        x++;
      } else {
        x1 -= cs2;
        x2 -= cs2;
        x--;
      }
    }
    while (true) {
      if (y1 <= aLocation.y && aLocation.y <= y2) {
        break;
      }
      if (aLocation.y > 0) {
        y1 += cs2;
        y2 += cs2;
        y++;
      } else {
        y1 -= cs2;
        y2 -= cs2;
        y--;
      }
    }
    while (true) {
      if (z1 <= aLocation.z && aLocation.z <= z2) {
        break;
      }
      if (aLocation.z > 0) {
        z1 += cs2;
        z2 += cs2;
        z++;
      } else {
        z1 -= cs2;
        z2 -= cs2;
        z--;
      }
    }
    return new Vector3f(x, y, z).mult(2 * theChunkSize);
  }

  /**
   * All loaded chunks have a three-dimensional position in the chunk-matrix.
   * 
   * <pre>
   *       _________
   *      /__/__/__/|                            
   *     /__/__/__/|/
   *    /__/__/__/|/
   *    |__|__|__|/
   *           _________
   *          /__/__/__/|
   *         /__/_X/__/|/
   *        /__/__/__/|/
   *        |__|__|__|/
   *               _________
   *              /__/__/__/|
   *             /__/__/__/|/
   *            /__/__/__/|/
   *            |__|__|__|/
   * 
   * </pre>
   * 
   * The chunk in the center of the cube is the {@link #chunkCenter}.
   */
  private final Map<Vector3f, TerrainChunk> loadedChunks = Maps.newHashMap();

  /**
   * To fill chunks with {@link TerrainElement}S.
   */
  private final TerrainLoader loader;

  /**
   * Size of each chunk.
   */
  private final float chunkSize;

  /**
   * @param theLoader
   */
  public OpenWorldTerrainChunk(final TerrainLoader theLoader) {
    this(theLoader, DEFAULT_CHUNK_SIZE);
  }

  /**
   * @param theLoader
   * @param theChunkSize
   */
  public OpenWorldTerrainChunk(final TerrainLoader theLoader, final float theChunkSize) {
    loader = theLoader;
    chunkSize = theChunkSize;
    // load the first 9 chunks ... later the first 27
    LOG.trace("Load initial chunks ...");
    getTerrainChunk(Vector3f.ZERO, false);
    LOG.trace("Load initial chunks finished");
  }

  @Override
  public void addTerrainElement(final TerrainElement aTerrainElement) {
    aTerrainElement.getCenter();
  }

  @Override
  public void addTerrainElement(final Vector3f theLocation, final BloxelType theBoxelType) {
    getTerrainChunk(theLocation, false).addTerrainElement(theLocation, theBoxelType);
  }

  private TerrainChunk createChunk(final Vector3f aLocation) {
    final TerrainChunk chunk = new TerrainChunkImpl(aLocation, chunkSize, loader);
    LOG.trace(String.format("Create a new chunk for location %s : %s", aLocation, chunk));
    return chunk;
  }

  public void debug(final Node terrainDebugNode, final AssetManager assetManager) {
    terrainDebugNode.detachAllChildren();
    final Set<Entry<Vector3f, TerrainChunk>> entrySet = loadedChunks.entrySet();
    for (final Entry<Vector3f, TerrainChunk> terrainChunkEntry : entrySet) {
      final TerrainChunk tc = terrainChunkEntry.getValue();
      final BoundingBox tcbb = tc.getBoundingBox();
      final Geometry g = new Geometry("debug", new Box(tcbb.getXExtent(), tcbb.getYExtent(), tcbb.getZExtent()));
      g.setLocalTranslation(tcbb.getCenter());
      final Material material = new Material(assetManager, "Common/MatDefs/Misc/WireColor.j3md");
      material.setColor("Color", new ColorRGBA(tcbb.getCenter().x % 255, tcbb.getCenter().y % 255,
          tcbb.getCenter().z % 255, 1f));
      g.setMaterial(material);
      terrainDebugNode.attachChild(g);
    }
  }

  @Override
  public BoundingBox getBoundingBox() {
    return new BoundingBox(Vector3f.NEGATIVE_INFINITY, Vector3f.POSITIVE_INFINITY);
  }

  @Override
  public Vector3f getCenter() {
    return Vector3f.ZERO;
  }

  /**
   * Calculate the center position of a {@link TerrainChunk} which contains the given point <code>aLocation</code>. The
   * returned {@link Vector3f} would be used to get the right {@link TerrainChunk}.
   * 
   * @param aLocation
   *          in world coordinates
   * @return the location of a {@link TerrainChunk} which contains the given position
   */
  private Vector3f getChunkLocation(final Vector3f aLocation) {
    final Vector3f chunkLocation = calcChunkLocation(aLocation, chunkSize);
    LOG.trace(String.format("Convert location %s to chunk location %s ( chunk terrain size %s)", aLocation,
        chunkLocation, chunkSize));
    return chunkLocation;
  }

  @Override
  public int getNumberOfElements() {
    int result = 0;
    for (final TerrainChunk tc : loadedChunks.values()) {
      result += tc.getNumberOfElements();
    }
    return result;
  }

  /**
   * This method will find the correct {@link TerrainChunk} which contains the given position. If the loaded chunks
   * doesn't contain the position more chunks must loaded, still "visible" chunks must moved (x,y,z), "invisible" chunks
   * must persists etc.
   * 
   * @param aLocation
   *          the absolute location in the terrain universe
   * @return
   */
  private TerrainChunk getTerrainChunk(final Vector3f aLocation, final boolean fetch) {
    final Vector3f chunkLocation = getChunkLocation(aLocation);
    TerrainChunk chunk = loadedChunks.get(chunkLocation);
    LOG.trace(String.format("Found chunk for location %s : %s", chunkLocation, chunk));
    if (chunk == null) {
      chunk = createChunk(chunkLocation);
      loadedChunks.put(chunkLocation, chunk);
      if (fetch) {
        //
        // |---|---|---|
        // | 2 | 6 | 4 |
        // |---|---|---|
        // | 0 | X | 1 |
        // |---|---|---|
        // | 3 | 7 | 5 |
        // |---|---|---|
        //
        final float x = chunkLocation.x;
        final float y = chunkLocation.y;
        final float z = chunkLocation.z;
        final Vector3f chunk0 = new Vector3f(x - chunkSize * 2, y, z); // -x
        final Vector3f chunk1 = new Vector3f(x + chunkSize * 2, y, z); // x+
        final Vector3f chunk2 = new Vector3f(x - chunkSize * 2, y, z + chunkSize * 2); // -x,z+
        final Vector3f chunk3 = new Vector3f(x - chunkSize * 2, y, z - chunkSize * 2); // -x,-z
        final Vector3f chunk4 = new Vector3f(x + chunkSize * 2, y, z + chunkSize * 2); // x+,z+
        final Vector3f chunk5 = new Vector3f(x + chunkSize * 2, y, z - chunkSize * 2); // x+,-z
        final Vector3f chunk6 = new Vector3f(x, y, z - chunkSize * 2); // -z
        final Vector3f chunk7 = new Vector3f(x, y, z + chunkSize * 2); // z+
        final ImmutableList<Vector3f> chunks = ImmutableList.of(chunk0, chunk1, chunk2, chunk3, chunk4, chunk5, chunk6,
            chunk7);
        for (final Vector3f v : chunks) {
          if (loadedChunks.containsKey(v)) {
            // already loaded
            LOG.trace(String.format("Chunk at position %s already loaded", v));
            continue;
          }
          LOG.trace(String.format("Chunk at position %s not loaded yet", v));
          loadedChunks.put(v, createChunk(v));
        }
      }
    }
    return chunk;
  }

  @Override
  public Collection<TerrainElement> getTerrainElements() {
    final List<TerrainElement> result = Lists.newArrayList();
    for (final TerrainChunk tc : loadedChunks.values()) {
      result.addAll(tc.getTerrainElements());
    }
    return result;
  }

  @Override
  public Collection<TerrainElement> getTerrainElements(final Vector3f thePlayerLocation) {
    return getTerrainElements(thePlayerLocation, Vector3f.ZERO);
  }

  @Override
  public Collection<TerrainElement> getTerrainElements(final Vector3f thePlayerLocation,
      final Vector3f thePlayerDirection) {
    final long start = System.currentTimeMillis();
    final Set<TerrainElement> result = Sets.newHashSet();
    final TerrainChunk terrainChunk = getTerrainChunk(thePlayerLocation, false);
    Collection<TerrainElement> terrainElements = terrainChunk.getTerrainElements();
    result.addAll(terrainElements);
    LOG.debug("get elements time: {}", (System.currentTimeMillis() - start));
    //
    // |---|---|---|
    // | 2 | 6 | 4 |
    // |---|---|---|
    // | 0 | X | 1 |
    // |---|---|---|
    // | 3 | 7 | 5 |
    // |---|---|---|
    //
    final Vector3f chunkLocation = terrainChunk.getCenter();
    final float x = chunkLocation.x;
    final float y = chunkLocation.y;
    final float z = chunkLocation.z;
    final Vector3f chunk0 = new Vector3f(x - chunkSize * 2, y, z); // -x
    final Vector3f chunk1 = new Vector3f(x + chunkSize * 2, y, z); // x+
    final Vector3f chunk2 = new Vector3f(x - chunkSize * 2, y, z + chunkSize * 2); // -x,z+
    final Vector3f chunk3 = new Vector3f(x - chunkSize * 2, y, z - chunkSize * 2); // -x,-z
    final Vector3f chunk4 = new Vector3f(x + chunkSize * 2, y, z + chunkSize * 2); // x+,z+
    final Vector3f chunk5 = new Vector3f(x + chunkSize * 2, y, z - chunkSize * 2); // x+,-z
    final Vector3f chunk6 = new Vector3f(x, y, z - chunkSize * 2); // -z
    final Vector3f chunk7 = new Vector3f(x, y, z + chunkSize * 2); // z+
    // und noch oben und unten ...
    final Vector3f chunk0a = new Vector3f(x, y - chunkSize * 2, z); // -y
    final Vector3f chunk1a = new Vector3f(x, y + chunkSize * 2, z); // y+
    final ImmutableList<Vector3f> chunks = ImmutableList.of(chunk0, chunk1, chunk2, chunk3, chunk4, chunk5, chunk6,
        chunk7, chunk0a, chunk1a);
    for (final Vector3f v : chunks) {
      final TerrainChunk tec = getTerrainChunk(v, false);
      terrainElements = tec.getTerrainElements();
      result.addAll(terrainElements);
    }
    return result;
  }

  @Override
  public void removeTerrainElement(final Vector3f theLocation) {
    final TerrainChunk c = getTerrainChunk(theLocation, false);
    LOG.trace(String.format("Remove element from chunk %s at position %s", c, theLocation));
    c.removeTerrainElement(theLocation);
  }

  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder();
    final Iterator<Entry<Vector3f, TerrainChunk>> iterator = loadedChunks.entrySet().iterator();
    while (iterator.hasNext()) {
      final Entry<Vector3f, TerrainChunk> entry = iterator.next();
      final Vector3f location = entry.getKey();
      final TerrainChunk chunk = entry.getValue();
      sb.append(String.format("chunk[%s] center: %s, entries:%d\n", location, chunk.getCenter(),
          chunk.getNumberOfElements()));
    }
    return sb.toString();
  }
}